/* Copyright (c) 2009-2010 David Forsythe.
 * See LICENSE file for license details.
 */

#include <archive.h>
#include <archive_entry.h>
#include <sys/param.h>
#include <stdio.h>
#include <fetch.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/queue.h>
#include <getopt.h>
#include <sys/utsname.h>

#include <libgen.h>

#include <pkg.h>
#include <pkg_freebsd.h>

#define ADD_NODEPS 0x00000001
#define ADD_NORECORD 0x00000002
#define ADD_NOSCRIPT 0x00000004
#define ADD_FORCE 0x00000008
#define ADD_DRYRUN 0x00000010
#define ADD_KEEP 0x00000020
#define ADD_REMOTE 0x00000040
#define ADD_VERBOSE 0x00000080
#define ADD_IGNOREDEPS 0x00000100
#define ADD_SLAVE 0x00000200

#define HAVE_URL 0x10000000

const char *prefix;
const char *ad; /* Archive directory. */

struct pkg_database *db;
struct pkg_repository *rp;
uint32_t flags;

struct pkg_target {
        char name[PATH_MAX];
        STAILQ_ENTRY(pkg_target) next;
};

struct add_artifact {
        char name[PATH_MAX];
        LIST_ENTRY(add_artifact) next;
};
LIST_HEAD(added, add_artifact) test_added;

STAILQ_HEAD(target_queue, pkg_target) targets;

void (*progress_handle) (const char *, size_t, size_t);

struct {
	uint32_t lowver;	/* Lowest version number to match */
	uint32_t hiver;	/* Highest version number to match */
	const char *directory;	/* Directory it lives in */
} releases[] = {
	{ 410000, 410000, "/packages-4.1-release" },
	{ 420000, 420000, "/packages-4.2-release" },
	{ 430000, 430000, "/packages-4.3-release" },
	{ 440000, 440000, "/packages-4.4-release" },
	{ 450000, 450000, "/packages-4.5-release" },
	{ 460000, 460001, "/packages-4.6-release" },
	{ 460002, 460099, "/packages-4.6.2-release" },
	{ 470000, 470099, "/packages-4.7-release" },
	{ 480000, 480099, "/packages-4.8-release" },
	{ 490000, 490099, "/packages-4.9-release" },
	{ 491000, 491099, "/packages-4.10-release" },
	{ 492000, 492099, "/packages-4.11-release" },
	{ 500000, 500099, "/packages-5.0-release" },
	{ 501000, 501099, "/packages-5.1-release" },
	{ 502000, 502009, "/packages-5.2-release" },
	{ 502010, 502099, "/packages-5.2.1-release" },
	{ 503000, 503099, "/packages-5.3-release" },
	{ 504000, 504099, "/packages-5.4-release" },
	{ 505000, 505099, "/packages-5.5-release" },
	{ 600000, 600099, "/packages-6.0-release" },
	{ 601000, 601099, "/packages-6.1-release" },
	{ 602000, 602099, "/packages-6.2-release" },
	{ 603000, 603099, "/packages-6.3-release" },
	{ 604000, 604099, "/packages-6.4-release" },
	{ 700000, 700099, "/packages-7.0-release" },
	{ 701000, 701099, "/packages-7.1-release" },
	{ 702000, 702099, "/packages-7.2-release" },
	{ 800000, 800499, "/packages-8.0-release" },
	{ 300000, 399000, "/packages-3-stable" },
	{ 400000, 499000, "/packages-4-stable" },
	{ 502100, 502128, "/packages-5-current" },
	{ 503100, 599000, "/packages-5-stable" },
	{ 600100, 699000, "/packages-6-stable" },
	{ 700100, 799000, "/packages-7-stable" },
	{ 800500, 899000, "/packages-8-stable" },
	{ 900000, 999000, "/packages-9-current" },
	{ 0, 9999999, "/packages-current" },
	{ 0, 0, NULL }
};

const char *repos[] = {
        "ftp.freebsd.org/pub/FreeBSD/ports/",
        "ftp2.freebsd.org/pub/FreeBSD/ports/",
        NULL
};

static void parse_arguments(int, char **);
static int pkg_add(struct pkg_repository *, struct pkg_database *, 
        struct target_queue *, const char *, uint32_t, 
        void (*) (const char *, size_t, size_t));
static void clean_up(int);
static void usage(void);

static void
usage(void)
{
        exit(100);
}

static void
parse_arguments(int argc, char **argv)
{
        int o;

        static char opts[] = "hviIRfFnrp:P:SMt:C:K";
        static struct option longopts[] = {
                { "chroot",	required_argument,	NULL,		'C' },
                { "dry-run",	no_argument,		NULL,		'n' },
                { "force",	no_argument,		NULL,		'f' },
                { "help",	no_argument,		NULL,		'h' },
                { "keep",	no_argument,		NULL,		'K' },
                { "master",	no_argument,		NULL,		'M' },
                { "no-deps",	no_argument,		NULL,		'i' },
                { "no-record",	no_argument,		NULL,		'R' },
                { "no-script",	no_argument,		NULL,		'I' },
                { "prefix",	required_argument,	NULL,		'p' },
                { "remote",	no_argument,		NULL,		'r' },
                { "template",	required_argument,	NULL,		't' },
                { "slave",	no_argument,		NULL,		'S' },
                { "verbose",	no_argument,		NULL,		'v' },
                { NULL,         0,                      NULL,           0},
        };
        
        struct pkg_target *pt;

        flags = 0;
        while ((o = getopt_long(argc, argv, opts, longopts, NULL)) != -1) {
                switch (o) {
                case ('f'):
                        flags |= ADD_FORCE;
                        break;
                case ('K'):
                        flags |= ADD_KEEP;
                        break;
                case ('n'):
                        flags |= ADD_DRYRUN;
                        break;
                case ('p'):
                        prefix = optarg;
                        break;
                case ('r'):
                        flags |= ADD_REMOTE;
                        break;
                case ('v'):
                        flags |= ADD_VERBOSE;
                        break;
                case ('h'):
                default:
                        usage();
                        break;
                }
        }

        STAILQ_INIT(&targets);

        argv += optind;
        for (; *argv != NULL; ++argv) {
                pt = calloc(1, sizeof(*pt));
                strcpy(pt->name, *argv);
                STAILQ_INSERT_TAIL(&targets, pt, next);
        }

}

static void
clean_up(int r)
{
        exit(r);
}

static int
check_installed(struct pkg_database *_db, struct pkg *p, const char *name)
{
        pkg_database_rewind(_db);

        printf("check: %s\n", name);
        /* XXX: Fix skips. */
        while (pkg_database_get_next_pkg(_db, p, 0) == PKG_OK) {
                if (strcmp(pkg_freebsd_get_name(p), name) == 0) {
                        return (1);
                }
        }
        struct add_artifact *t;
        LIST_FOREACH(t, &test_added, next) {
                if (strcmp(t->name, name) == 0) {
                        return (1);
                }
        }
        return (0);
}

static void
progress(const char *name, size_t fetched, size_t total)
{
        printf("\r%s: %lu/%lu bytes", name, (long unsigned)fetched, 
                (long unsigned)total);
}


int
main(int argc, char **argv)
{
        int r;

        parse_arguments(argc, argv);
        
        ad = "pkgdl";

        db = pkg_freebsd_database_create();
        rp = pkg_freebsd_repository_create();

        r = pkg_add(rp, db, &targets, ad, flags, progress);
        clean_up(r);

        /* NOT REACHED. */
        return (0);
}

#define FLAGGED(x, y) (x & y)

static int
add_package(const char *_pkg, struct pkg_repository *_rp, struct pkg_database *_db, 
        const char *_ad, uint32_t _flags,
        void (*_progress) (const char *, size_t, size_t))
{
        struct archive *ar;
        struct archive_entry *ae;
        struct pkg *p;
        struct pkg *op;
        FILE *fp;
        int r;
        char *buf;
        ssize_t bufsz;
        struct pkg_dependency d;
        struct pkg_conflict c;

        char depname[PATH_MAX];

        /* For package fetch. */
        FILE *dl;
        char *aname;
        size_t asz;
        size_t track;

        int w;
        int f;

        char buffer[256];

        int fd;
        
        printf("_pkg = %s\n", _pkg);
        /* Grap the package from an open repository. */
        dl = pkg_repository_fetch_pkg(_rp, _pkg, &aname, &asz, 0);
        if (dl == NULL) {
                printf("pkg not found.\n");
                return (-1);
        }

        if (aname == NULL) {
                aname = strdup("TEMP_PACKAGE.tbz");
        }

        mkdir(_ad, 0700);
        chdir(_ad);

        warnx("opening: %s\n", aname);

        fd = open(aname, O_WRONLY | O_CREAT | O_TRUNC, 
                S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        if (fd < 0) {
                printf("could not open %s\n", aname);
                return (-1);
        }
        
        track = 0;
        while ((f = fread(buffer, 1, sizeof(buffer), dl)) > 0) {
                if ((w = write(fd, buffer, f)) != f) {
                        errx(1, "Read: %d Wrote: %d", f, w);
                }
                track += w;
                if (_progress != NULL) {
                        _progress(aname, track, asz);
                }
        }
        printf("\n");

        fp = fopen(aname, "r");
        if (fp == NULL) {
                printf("fp fail\n");
                return (-1);
        }

        ar = archive_read_new();
        archive_read_support_format_tar(ar);
        archive_read_support_compression_bzip2(ar);
        archive_read_support_compression_gzip(ar);
        
        warnx("starting extract.\n");

        r = archive_read_open_FILE(ar, fp);
        /* 
         * It looks like the old pkg_add checks gets the archive before it does
         * any db checks.  This makes sense, because a package may not be named
         * correctly (or rather, it might just not be named... its name).
         */

        /* First entry should be +CONTENTS. */
        r = archive_read_next_header(ar, &ae);
        if (strcmp(archive_entry_pathname(ae), "+CONTENTS") != 0) {
                errx(1, "Package contents invalid: +CONTENTS is not the first"
                " entry in the archive.");
                fclose(fp);
                return (-1);
        }

        warnx("reading contents");

        buf = malloc(archive_entry_size(ae) + 1);
        bufsz = archive_entry_size(ae);
        r = archive_read_data(ar, buf, archive_entry_size(ae));
        buf[bufsz + 1] = '\0';
        p = pkg_freebsd_create();
        op = pkg_freebsd_create();
        r = pkg_parse(p, buf, "CONTENTS");
        
        warnx("checking if %s already installed", _pkg);
        if (check_installed(db, op, pkg_freebsd_get_name(p))) {
                fclose(fp);
                printf("already installed!\n");
                return (-1);
        }
        
        warnx("checking conflicts for %s", _pkg);

        while (pkg_freebsd_get_next_conflict(p, &c) == PKG_OK) {
                /* Need to do a regex check for installed. */
                if (check_installed(db, op, 
                        pkg_freebsd_conflict_get_expression(&c))) {
                        /* Conflict alert. */
                        printf("conflicts with %s\n",
                                pkg_freebsd_conflict_get_expression(&c));
                        return (-1);
                }
        }

        while (pkg_freebsd_get_next_dependency(p, &d) == PKG_OK) {
                if (!check_installed(db, op,
                        pkg_freebsd_dependency_get_name(&d))) {
                        warnx("%s depends on %s\n", aname,
                                pkg_freebsd_dependency_get_name(&d));
                        printf("dep: %s\n", 
                                pkg_freebsd_dependency_get_name(&d));
                        strcpy(depname, "All/");
                        strcat(depname, pkg_freebsd_dependency_get_name(&d));
                        add_package(depname, _rp, _db, _ad, _flags, _progress);
                }
        }

        while (archive_read_next_header(ar, &ae) == ARCHIVE_OK) {
                if (/* pkg_meta_entry(ae) */ archive_entry_pathname(ae)[0] == '+') {
                        if (bufsz < archive_entry_size(ae)) {
                                /* XXX: Check for neg. */
                                buf = malloc(archive_entry_size(ae));
                                if (buf == NULL) {
                                        return (-1);
                                }
                                bufsz = archive_entry_size(ae);
                        }
                        r = archive_read_data(ar, buf, archive_entry_size(ae));
                        r = pkg_parse(p, buf, archive_entry_pathname(ae));
                } else {
                        pkg_freebsd_extract(p, ar, ae);
                }
        }
        
        printf("\n\nXXX: ADD: %s\n\n", basename(_pkg));
        struct add_artifact *add;
        add = calloc(1, sizeof(*add));
        strcpy(add->name, basename(_pkg));
        LIST_INSERT_HEAD(&test_added, add, next);

        return (0);
}

static const char *
get_repo_sub_path(void)
{
        uint32_t rel;
        
        rel = getosreldate();

        for (int i = 0; releases[i].directory != NULL; ++i) {
                if (rel >= releases[i].lowver && rel <= releases[i].hiver) {
                        return (releases[i].directory);
                }
        }
        return (NULL);
}

static const char *
get_repo_arch(void)
{
        static struct utsname n;
        if (uname(&n) < 0) {
                return (NULL);
        }
        return (n.machine);
}

/* 
 * Set up the repository and prepare for the add.  Use do_add to actually
 * install a package.
 */

static int
pkg_add(struct pkg_repository *_rp, struct pkg_database *_db,
        struct target_queue *_tq, const char *_ad, uint32_t _flags,
        void (*_progress) (const char *, size_t, size_t))
{
        int r;
        struct pkg_target *t;
        char name[PATH_MAX];
        char repo_site[PATH_MAX];
        const char *repo_sub;
        const char *repo_arch;

        /* 
         * XXX: To be clear: We could stream right to archive_open_FILE(), but
         * we want a bit of insurance against a lost connection.  So we pull
         * down the entire archive (even when we don't have keep set).
         */

        /* TODO: Dummy repo. */

        if (FLAGGED(_flags, ADD_REMOTE)) {
                repo_sub = get_repo_sub_path();
                repo_arch = get_repo_arch();
                if (repo_sub == NULL) {
                        errx(1, "Can't figure out relver");
                }
                /* XXX: We're lying for testing. */
                repo_sub = "/packages-8.1-release";
                rp = pkg_freebsd_repository_create();
                r = 0;
                for (int i = 0; repos[i] != NULL; ++i) {
                        /* O.O */
                        strcpy(repo_site, repos[i]);
                        strcat(repo_site, "/");
                        strcat(repo_site, repo_arch);
                        strcat(repo_site, "/");
                        strcat(repo_site, repo_sub);
                        strcat(repo_site, "/");
                        
                        r = pkg_repository_open(_rp, repo_site, 0);
                        if (r == PKG_OK) {
                                break;
                        }
                }
#if 0
                "ftp.freebsd.org/pub/FreeBSD/ports/amd64/packages-8.1-release/Latest/",
                0);
#endif
        } else {
                /* 
                 * This is the 'dummy repo' situation.  Just set a path and use
                 * the repository interface to fetch packages.
                 */
                rp = NULL;
                r = 0;
#if 0
                rp = pkg_freebsd_repository_path_create();
                r = 0;
#endif
        }
#if 0
        /* XXX: Something I want to play with. */
        else if (FLAGGED(_a->flags, ADD_LOCAL_REPO)){
                rp = pkg_freebsd_repository_local_create();
                r = pkg_repository_open(rp, packages, 0);
        }

#endif
        if (r != PKG_OK) {
                errx(1, "Could not open repository.\n");
        }
        
        pkg_database_open(db, "/var/db/pkg", 0);
        STAILQ_FOREACH(t, _tq, next) {
#if 0
                if (check_installed(db, p, t->target)) {
                        /* TODO: Act on installed. */
                        continue;
                }
#endif
                strcpy(name, "Latest/");
                strcat(name, t->name);
                if (add_package(name, _rp, _db, _ad, _flags, _progress) < 0) {
                        warnx("error adding %s", t->name);
                        break;
                }
        }

        pkg_repository_close(rp);
        pkg_repository_release(rp);

        return (0);
}
